﻿Imports System.Windows.Forms
Imports System.Drawing
Imports log4net
Imports log4net.Core
Imports log4net.Appender
Imports log4net.Util

Namespace log4net.Appender

    ''' <summary>
    ''' Appends logging events to a RichTextBox
    ''' </summary>
    ''' <remarks>
    ''' <para>
    ''' RichTextBoxAppender appends log events to a specified RichTextBox control.
    ''' It also allows the color, font and style of a specific type of message to be set.
    ''' </para>
    ''' <para>
    ''' When configuring the rich text box appender, mapping should be
    ''' specified to map a logging level to a text style. For example:
    ''' </para>
    ''' <code lang="XML" escaped="true">
    '''  <mapping>
    '''    <level value="DEBUG" />
    '''    <textColorName value="DarkGreen" />
    '''  </mapping>
    '''  <mapping>
    '''    <level value="INFO" />
    '''    <textColorName value="ControlText" />
    '''  </mapping>
    '''  <mapping>
    '''    <level value="WARN" />
    '''    <textColorName value="Blue" />
    '''  </mapping>
    '''  <mapping>
    '''    <level value="ERROR" />
    '''    <textColorName value="Red" />
    '''    <bold value="true" />
    '''    <pointSize value="10" />
    '''  </mapping>
    '''  <mapping>
    '''    <level value="FATAL" />
    '''    <textColorName value="Black" />
    '''    <backColorName value="Red" />
    '''    <bold value="true" />
    '''    <pointSize value="12" />
    '''    <fontFamilyName value="Lucida Console" />
    '''  </mapping> 
    ''' </code>
    ''' <para>
    ''' The Level is the standard log4net logging level. TextColorName and BackColorName should match
    ''' a value of the System.Drawing.KnownColor enumeration. Bold and/or Italic may be specified, using
    ''' <code>true</code> or <code>false</code>. FontFamilyName should match a font available on the client,
    ''' but if it's not found, the control's font will be used.
    ''' </para>
    ''' <para>
    ''' The RichTextBox property has to be set in code. The most straightforward way to accomplish
    ''' this is in the Load event of the Form containing the control.
    ''' <code lang="C#">
    ''' private void MainForm_Load(object sender, EventArgs e)
    ''' {
    '''    log4net.Appender.RichTextBoxAppender.SetRichTextBox(logRichTextBox, "MainFormRichTextAppender");
    ''' }
    ''' </code>
    ''' </para>
    ''' </remarks>
    ''' <author>Stephanie Giovannini</author>
    Public Class RichTextBoxAppender
        Inherits AppenderSkeleton


#Region "Public Instance Constructor"

        ''' <summary>
        ''' Initializes a new instance of the <see cref="RichTextBoxAppender" /> class.
        ''' </summary>
        ''' <remarks>
        ''' The instance of the <see cref="RichTextBoxAppender" /> class can be assigned
        ''' a <see cref="System.Windows.Forms.RichTextBox" /> to write to.
        ''' </remarks>
        Public Sub New()
            MyBase.New()
        End Sub

#End Region

#Region "Public Instance Properties and Methods"

        ''' <summary>
        ''' Reference to RichTextBox that displays logging events
        ''' </summary>
        ''' <remarks>
        ''' <para>
        ''' This property is a reference to the RichTextBox control
        ''' that will display logging events.
        ''' </para>
        ''' <para>If RichTextBox is null, no logging events will be displayed.</para>
        ''' <para>RichTextBox will be set to null when the control's containing
        ''' Form is closed.</para>
        ''' </remarks>
        Public Property RichTextBox() As RichTextBox
            Get
                Return _richtextBox
            End Get
            Set(ByVal value As RichTextBox)
                If Not Object.ReferenceEquals(value, _richtextBox) Then
                    If _containerForm IsNot Nothing Then
                        RemoveHandler _containerForm.FormClosed, New FormClosedEventHandler(AddressOf containerForm_FormClosed)
                        _containerForm = Nothing
                    End If
                    If value IsNot Nothing Then
                        value.[ReadOnly] = True
                        value.HideSelection = False
                        _containerForm = value.FindForm()
                        AddHandler _containerForm.FormClosed, New FormClosedEventHandler(AddressOf containerForm_FormClosed)
                    End If
                    _richtextBox = value
                End If
            End Set
        End Property


        ''' <summary>
        ''' Add a mapping of level to text style - done by the config file
        ''' </summary>
        ''' <param name="mapping">The mapping to add</param>
        ''' <remarks>
        ''' <para>
        ''' Add a <see cref="LevelTextStyle"/> mapping to this appender.
        ''' Each mapping defines the text style for a level.
        ''' </para>
        ''' </remarks>
        Public Sub AddMapping(ByVal mapping As LevelTextStyle)
            _levelMapping.Add(mapping)
        End Sub

        ''' <summary>
        ''' Maximum number of characters in control before it is cleared
        ''' </summary>
        Public Property MaxBufferLength() As Integer
            Get
                Return _maxTextLength
            End Get
            Set(ByVal value As Integer)
                If value > 0 Then
                    _maxTextLength = value
                End If
            End Set
        End Property

#End Region


#Region "Override Implementation of AppenderSkeleton"

        ''' <summary>
        ''' Initialize the options for this appender
        ''' </summary>
        ''' <remarks>
        ''' <para>
        ''' Initialize the level to text style mappings set on this appender.
        ''' </para>
        ''' </remarks>
        Public Overrides Sub ActivateOptions()
            MyBase.ActivateOptions()

            _levelMapping.ActivateOptions()

        End Sub


        ''' <summary>
        ''' This method is called by the <see cref="AppenderSkeleton.DoAppend(log4net.Core.LoggingEvent)"/> method.
        ''' </summary>
        ''' <param name="loggingEvent">The event to log.</param>
        ''' <remarks>
        ''' <para>
        ''' Writes the event to the RichTextBox control, if set.
        ''' </para>
        ''' <para>
        ''' The format of the output will depend on the appender's layout.
        ''' </para>
        ''' <para>
        ''' This method can be called from any thread.
        ''' </para>
        ''' </remarks>
        Protected Overrides Sub Append(ByVal LoggingEvent As LoggingEvent)

            If _richtextBox IsNot Nothing Then
                If _richtextBox.InvokeRequired Then
                    _richtextBox.Invoke(New UpdateControlDelegate(AddressOf UpdateControl), New Object() {LoggingEvent})
                Else
                    UpdateControl(LoggingEvent)
                End If
            End If

        End Sub

        ''' <summary>
        ''' Delegate used to invoke UpdateControl
        ''' </summary>
        ''' <param name="loggingEvent">The event to log</param>
        ''' <remarks>This delegate is used when UpdateControl must be
        ''' called from a thread other than the thread that created the
        ''' RichTextBox control.</remarks>
        Private Delegate Sub UpdateControlDelegate(ByVal loggingEvent As LoggingEvent)

        ''' <summary>
        ''' Add logging event to configured control
        ''' </summary>
        ''' <param name="loggingEvent">The event to log</param>
        Private Sub UpdateControl(ByVal loggingEvent As LoggingEvent)

            ' There may be performance issues if the buffer gets too long
            ' So periodically clear the buffer
            If _richtextBox.TextLength > _maxTextLength Then
                _richtextBox.Clear()
                _richtextBox.AppendText(String.Format("(earlier messages cleared because log length exceeded maximum of {0})" & vbLf & vbLf, _maxTextLength))
            End If

            ' look for a style mapping
            Dim selectedStyle As LevelTextStyle = TryCast(_levelMapping.Lookup(loggingEvent.Level), LevelTextStyle)
            If selectedStyle IsNot Nothing Then

                ' set the colors of the text about to be appended
                _richtextBox.SelectionBackColor = selectedStyle.BackColor
                _richtextBox.SelectionColor = selectedStyle.TextColor

                ' alter selection font as much as necessary
                ' missing settings are replaced by the font settings on the control
                If selectedStyle.Font IsNot Nothing Then

                    ' set Font Family, size and styles
                    _richtextBox.SelectionFont = selectedStyle.Font

                ElseIf selectedStyle.PointSize > 0 AndAlso _richtextBox.Font.SizeInPoints <> selectedStyle.PointSize Then

                    ' use control's font family, set size and styles
                    Dim size As Single = If(selectedStyle.PointSize > 0.0F, selectedStyle.PointSize, _richtextBox.Font.SizeInPoints)

                    _richtextBox.SelectionFont = New Font(_richtextBox.Font.FontFamily.Name, size, selectedStyle.FontStyle)

                ElseIf _richtextBox.Font.Style <> selectedStyle.FontStyle Then

                    ' use control's font family and size, set styles
                    _richtextBox.SelectionFont = New Font(_richtextBox.Font, selectedStyle.FontStyle)

                End If
            End If

            _richtextBox.AppendText(RenderLoggingEvent(loggingEvent))

        End Sub



        ''' <summary>
        ''' Remove reference to RichTextBox when container form is closed
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <param name="e"></param>
        Private Sub containerForm_FormClosed(ByVal sender As Object, ByVal e As FormClosedEventArgs)

            RichTextBox = Nothing

        End Sub



        ''' <summary>
        ''' Remove references to container form
        ''' </summary>
        Protected Overrides Sub OnClose()
            MyBase.OnClose()

            If _containerForm IsNot Nothing Then
                RemoveHandler _containerForm.FormClosed, New FormClosedEventHandler(AddressOf containerForm_FormClosed)
                _containerForm = Nothing
            End If

        End Sub


        ''' <summary>
        ''' This appender requires a <see cref="Layout"/> to be set.
        ''' </summary>
        ''' <value><c>true</c></value>
        ''' <remarks>
        ''' <para>
        ''' This appender requires a <see cref="Layout"/> to be set.
        ''' </para>
        ''' </remarks>
        Protected Overrides ReadOnly Property RequiresLayout() As Boolean
            Get
                Return True
            End Get
        End Property

#End Region



#Region "Public Static Methods"

        ''' <summary>
        ''' Assign a RichTextBox to a RichTextBoxAppender
        ''' </summary>
        ''' <param name="richTextBox">Reference to RichTextBox control that will display logging events</param>
        ''' <param name="appenderName">Name of RichTextBoxAppender (case-sensitive)</param>
        ''' <returns>True if a RichTextBoxAppender named <code>appenderName</code> was found</returns>
        ''' <remarks>
        ''' <para>This method sets the RichTextBox property of the RichTextBoxAppender
        ''' in the default repository with <code>Name == appenderName</code>.</para>
        ''' </remarks>
        ''' <example>
        ''' <code lang="C#">
        ''' private void MainForm_Load(object sender, EventArgs e)
        ''' {
        '''    log4net.Appender.RichTextBoxAppender.SetRichTextBox(logRichTextBox, "MainFormRichTextAppender");
        ''' }
        ''' </code>
        ''' </example>
        Public Shared Function SetRichTextBox(ByVal richTextBox As RichTextBox, ByVal appenderName As String) As Boolean

            If appenderName Is Nothing Then
                Return False
            End If

            Dim appenders As IAppender() = LogManager.GetRepository().GetAppenders()
            For Each appender As IAppender In appenders
                If appender.Name = appenderName Then
                    If TypeOf appender Is RichTextBoxAppender Then
                        DirectCast(appender, RichTextBoxAppender).RichTextBox = richTextBox
                        Return True
                    End If
                    Exit For
                End If
            Next

            Return False

        End Function


#End Region



#Region "Private Instance Fields"

        ''' <summary>
        ''' Reference to RichTextBox control that will display log events
        ''' </summary>
        Private _richtextBox As RichTextBox = Nothing

        ''' <summary>
        ''' Reference to Form that contains <code>_richtextBox</code>
        ''' </summary>
        Private _containerForm As Form = Nothing

        ''' <summary>
        ''' Mapping from level object to text style
        ''' </summary>
        Private _levelMapping As New LevelMapping()

        ''' <summary>
        ''' Maximum length of RichTextBox buffer
        ''' </summary>
        Private _maxTextLength As Integer = 100000


#End Region



#Region "LevelTextStyle LevelMappingEntry"


        ''' <summary>
        ''' A class to act as a mapping between the level that a logging call is made at and
        ''' the text style in which it should be displayed.
        ''' </summary>
        ''' <remarks>
        ''' <para>
        ''' Defines the mapping between a level and the text style in which it should be displayed..
        ''' </para>
        ''' </remarks>
        Public Class LevelTextStyle
            Inherits LevelMappingEntry

            Private _textColorName As String = "ControlText"
            Private _backColorName As String = "ControlLight"
            Private _textColor As Color
            Private _backColor As Color
            Private _fontStyle As FontStyle = FontStyle.Regular
            Private _pointSize As Single = 0.0F
            Private _bold As Boolean = False
            Private _italic As Boolean = False
            Private _fontFamilyName As String = Nothing
            Private _font As Font = Nothing

            ''' <summary>
            ''' Name of a KnownColor used for text
            ''' </summary>
            Public Property TextColorName() As String
                Get
                    Return _textColorName
                End Get
                Set(ByVal value As String)
                    _textColorName = value
                End Set
            End Property


            ''' <summary>
            ''' Name of a KnownColor used as text background
            ''' </summary>
            Public Property BackColorName() As String
                Get
                    Return _backColorName
                End Get
                Set(ByVal value As String)
                    _backColorName = value
                End Set
            End Property


            ''' <summary>
            ''' Name of a font family
            ''' </summary>
            Public Property FontFamilyName() As String
                Get
                    Return _fontFamilyName
                End Get
                Set(ByVal value As String)
                    _fontFamilyName = value
                End Set
            End Property


            ''' <summary>
            ''' Display level in bold style
            ''' </summary>
            Public Property Bold() As Boolean
                Get
                    Return _bold
                End Get

                Set(ByVal value As Boolean)
                    _bold = value
                End Set
            End Property


            ''' <summary>
            ''' Display level in italic style
            ''' </summary>
            Public Property Italic() As Boolean
                Get
                    Return _italic
                End Get
                Set(ByVal value As Boolean)
                    _italic = value
                End Set
            End Property


            ''' <summary>
            ''' Font size of level, 0 to use default
            ''' </summary>
            Public Property PointSize() As Single
                Get
                    Return _pointSize
                End Get
                Set(ByVal value As Single)
                    _pointSize = value
                End Set
            End Property


            ''' <summary>
            ''' Initialize the options for the object
            ''' </summary>
            ''' <remarks>Parse the properties</remarks>
            Public Overrides Sub ActivateOptions()
                MyBase.ActivateOptions()

                _textColor = Color.FromName(_textColorName)
                _backColor = Color.FromName(_backColorName)
                If _bold Then
                    _fontStyle = _fontStyle Or FontStyle.Bold
                End If

                If _italic Then
                    _fontStyle = _fontStyle Or FontStyle.Italic
                End If

                If _fontFamilyName IsNot Nothing Then
                    Dim size As Single = If(_pointSize > 0.0F, _pointSize, 8.25F)

                    Try
                        _font = New Font(_fontFamilyName, size, _fontStyle)
                    Catch generatedExceptionName As Exception
                        _font = Nothing
                    End Try
                End If

            End Sub


            Friend ReadOnly Property TextColor() As Color
                Get
                    Return _textColor
                End Get
            End Property

            Friend ReadOnly Property BackColor() As Color
                Get
                    Return _backColor
                End Get
            End Property

            Friend ReadOnly Property FontStyle() As FontStyle
                Get
                    Return _fontStyle
                End Get
            End Property

            Friend ReadOnly Property Font() As Font
                Get
                    Return _font
                End Get
            End Property

        End Class


#End Region

    End Class

End Namespace


